Generalize recipes and implement them#1449
Generalize recipes and implement them#1449Limeth wants to merge 37 commits intoSpongePowered:bleedingfrom
Conversation
|
The SpongeAPI part is now in the state of what I'd like to propose. Comments and reviews very welcome. Continuing by attempting to implement the SpongeCommon part. |
| * @throws IllegalArgumentException If the aisle does not contain | ||
| * the specified character symbol | ||
| */ | ||
| Builder where(char symbol, @Nullable ItemStackSnapshot ingredient) throws IllegalArgumentException; |
There was a problem hiding this comment.
- I guess this can be default implemented as well.
There was a problem hiding this comment.
It cannot, the default item matching behavior in vanilla Minecraft is defined in NMS code and I needed to mimic it. Can't do that with SpongeAPI. Maybe I should create a method in SpongeCommon that mimics the behavior and expose it via SpongeAPI?
There was a problem hiding this comment.
Don't accept null as ingredient. Ask users to use ItemStackSnapshot.NONE
There was a problem hiding this comment.
@Limeth What part of item matching is not possible in SpongeAPI?
There was a problem hiding this comment.
@Limeth: That link seems to be broken. Can the implementation take care of passing in ItemStackSnapshot.NONE to the predicate, if that's what the issue is?
| /** | ||
| * @return {@code width * height} for shaped recipes, or the number of ingredients for shapeless recipes | ||
| */ | ||
| int getRecipeSize(); |
There was a problem hiding this comment.
When would i/the impl need this method?
There was a problem hiding this comment.
The implementation on the NMS side assigns priority to recipes depending on this value. You would use this to change that priority.
There was a problem hiding this comment.
Please document this behaviour in the javadocs.
| * Removes the given CraftingRecipe from registration in this registry. | ||
| * | ||
| * @param recipe The Recipe to unregister | ||
| * @param recipe The CraftingRecipe to unregister |
There was a problem hiding this comment.
The javadocs mention crafting recipes but the generic type allows plain recipes as well.
There was a problem hiding this comment.
Thanks, didn't catch this one after auto-refactoring it.
| * @return An {@link ItemStack} or {@link Optional#empty()} if the given | ||
| * {@link GridInventory} does not match this recipe's requirements. | ||
| */ | ||
| Optional<ItemStack> getResult(GridInventory grid); |
There was a problem hiding this comment.
Here you get the results but how do you actually consume the items? Is it required to reduce the number by one? How about Blood Magic's magicians orbs that act as a mere catalyst and thus is not consumed? Or recipes where a tool is used and the tool only takes some damage?
There was a problem hiding this comment.
The removal of items from the crafting grid is handled by the CraftingManager in NMS. To preserve items in the crafting grid, provide them via #getRemainingItems(GridInventory).
| * @return Whether this {@param ingredient} can be used to craft the result | ||
| */ | ||
| default boolean isValid(ItemStackSnapshot ingredient) { | ||
| // ItemStackSnapshot#NONE gets replaced at runtime |
There was a problem hiding this comment.
The argument is non-null by default, as defined in the package-info.java file in the package.
There was a problem hiding this comment.
Ah, turns out @Nonnull is only used for static analysis.
|
|
||
| @Override | ||
| default boolean isValid(GridInventory grid, World world) { | ||
| List<Predicate<ItemStackSnapshot>> predicates = Lists.newLinkedList(getIngredientPredicates()); |
| * @param y The y coordinate counted from the top | ||
| * @return The ingredient predicate at this position defined by the aisle | ||
| */ | ||
| default Optional<Predicate<ItemStackSnapshot>> getIngredientPredicate(int x, int y) { |
There was a problem hiding this comment.
Why are these methods default implemented?
There was a problem hiding this comment.
So people who want to implement this interface themselves don't have to implement this method.
There was a problem hiding this comment.
Is that not what builders are for? if you're cluttering up the API to make it a little easier for the tiny percent of people why have to implement this in a plugin then you can remove all of these default implementations.
The only things that should be default implemented is when its a trivial helper method.
There was a problem hiding this comment.
I'd find it incredibly useful to not have to write these myself, what is the issue exactly? All ShapedCraftingRecipes should behave like this, this is why they are default-implemented. And in corner cases, it also makes it possible to override them. It sounds like a win-win scenario to me. Note that all CraftingRecipes have an isValid method.
There was a problem hiding this comment.
I'd much rather have the least amount of actual implementation in the API. That being said, this is implementation, not API.
| * or you can simply use the {@link ShapelessCraftingRecipe.Builder} or {@link ShapedCraftingRecipe.Builder}.</p> | ||
| */ | ||
| public interface CraftingRecipe extends Recipe { | ||
| /** |
There was a problem hiding this comment.
Missing new line after body start.
| } | ||
|
|
||
| /** | ||
| * @return An unmodifiable copy of the original aisle |
| int gapHeight = grid.getRows() - getHeight(); | ||
|
|
||
| // Shift the aisle along the grid wherever possible | ||
| for(int offsetX = 0; offsetX <= gapWidth; offsetX++) { |
There was a problem hiding this comment.
Do I have my checkstyle plugin set up incorrectly? I am not getting the warnings here.
|
-_- #1419 |
|
Began implementing the SpongeCommon part. |
Deamon5550
left a comment
There was a problem hiding this comment.
Still some implementation in here, a few missing or misworded javadocs, and you need to go through with your IDE and fix the line wrapping of your javadocs its all over the place.
| */ | ||
| boolean isValid(GridInventory grid); | ||
|
|
||
| /** |
| import java.util.Optional; | ||
|
|
||
| /** | ||
| * <p>A CraftingRecipe represents some craftable recipe in the game.</p> |
There was a problem hiding this comment.
Don't need paragraph tags for the first paragraph.
| * <p>The requirements of a CraftingRecipe can be general, they just have to | ||
| * eventually return a boolean given an itemgrid.</p> | ||
| * | ||
| * <p>You can implement it manually to suit your creative needs, |
There was a problem hiding this comment.
Skip this bit, its confusing and its stating the obvious.
| * This method is preferred over the {@link #getExemplaryResult()} method, | ||
| * as it customizes the result further depending on the specified | ||
| * input {@param grid}. | ||
| * It is advised to use the output of {@link #getExemplaryResult()}, |
There was a problem hiding this comment.
Either join this to the previous line or make it a new paragraph.
| * | ||
| * @return An unmodifiable copy of the original aisle | ||
| */ | ||
| List<String> getAisle(); |
There was a problem hiding this comment.
The aisle stuff is great for the builder but it doesn't have a place in the actual recipe. You already have width and height information and a method for getting the predicate for a position, keeping the aisle stuff around is useless as translating the characters is way more work than just using the methods that take an x, y position.
| * @return The result of smelting the {@param ingredient} {@link ItemStack}, if the {@param ingredient} is valid | ||
| */ | ||
| default Optional<ItemStack> getResult(ItemStackSnapshot ingredient) { | ||
| Preconditions.checkNotNull(ingredient, "The ingredient must not be null"); |
There was a problem hiding this comment.
Remove this implementation
| import java.util.Optional; | ||
|
|
||
| /** | ||
| * A general interface for furnace recipes. |
There was a problem hiding this comment.
Whatever you are wrapping the javadocs in this whole file to it isn't 80 characters.
| */ | ||
| ItemStackSnapshot getExemplaryIngredient(); | ||
|
|
||
| /** |
| } | ||
|
|
||
| /** | ||
| * This method is preferred over the {@link #getExemplaryResult()} method, |
There was a problem hiding this comment.
Shouldn't say preferred, this is the method to use.
| } | ||
|
|
||
| /** | ||
| * @param ingredient The ingredient being smelted |
|
@Deamon5550 Thanks for the review. Should this be highlighted by the checkstyle plugin? I am not getting any warnings like those you named. |
|
The checkstyle and formatter do their best to help you but common sense is always the best. |
|
@Deamon5550 Fixed. |
ST-DDT
left a comment
There was a problem hiding this comment.
Nice API design. I guess this will become quite handy.
| /** | ||
| * Builds a simple furnace recipe | ||
| */ | ||
| interface Builder extends ResettableBuilder<SmeltingRecipe, Builder> { |
There was a problem hiding this comment.
- Missing javadocs for all methods inside this builder.
| * or {@link Optional#empty()} if the given | ||
| * {@link GridInventory} does not match this recipe's requirements. | ||
| */ | ||
| Optional<List<ItemStack>> getRemainingItems(GridInventory grid, World world); |
There was a problem hiding this comment.
I quite unsure about this one.
Wouldn't be a Optional<CraftingResult> better here?
Because currently you have to check the validity of the recipe twice (one for the result stack and one time for the remaining stacks).
If you use a CraftingResult which would store both of them at once you could skip one validity execution.
There was a problem hiding this comment.
Thanks for the suggestion. Originally, the methods used to have different parameters, I didn't notice they could be merged together after changing them, thanks for pointing this out!
|
Edit: Since users would have to implement |
| */ | ||
| package org.spongepowered.api.item.recipe.smelting; | ||
|
|
||
| public interface SmeltingRecipeRegistry { |
There was a problem hiding this comment.
You forgot to extend the RecipeRegistry.
| * @return The amount of experience released after completing this recipe, | ||
| * if the {@param ingredient} is valid | ||
| */ | ||
| Optional<Double> getExperience(ItemStackSnapshot ingredient); |
There was a problem hiding this comment.
Use java.util.OptionalDouble
There was a problem hiding this comment.
There was a problem hiding this comment.
There was a problem hiding this comment.
But there are no map/flatMap functions for chaining in OptionalDouble. Is performance really at stake here?
There was a problem hiding this comment.
Why do you have 2 wrappers if you can just have one @Limeth?
There was a problem hiding this comment.
Exactly for the reason I stated above.
There was a problem hiding this comment.
You don't really need to map for doubles.
There was a problem hiding this comment.
liach is referring to .map(Function<T,R>).
Well if the absent result result is equivalent to "does not grant experience" then it could just return primitive double zero
| * @param result The resultant snapshot | ||
| * @return The builder | ||
| */ | ||
| Builder result(@Nullable ItemStackSnapshot result); |
| */ | ||
| ShapedCraftingRecipe build(); | ||
| } | ||
| } |
| return Sponge.getRegistry().createBuilder(Builder.class); | ||
| } | ||
|
|
||
|
|
|
This is more a question of curiosity, but does this support information such as Lore, Names, Enchantments, etc, on recipe items? |
|
@me4502 Yes, you can implement that ingredient predicate yourself and only return |
| * is completed | ||
| * @return This builder, for chaining | ||
| */ | ||
| Builder experience(@Nullable Double experience); |
There was a problem hiding this comment.
What does null stand for here? Use default?
| * @return This builder, for chaining | ||
| */ | ||
| default Builder result(@Nullable ItemStack result) { | ||
| return result(result != null ? result.createSnapshot() : null); |
There was a problem hiding this comment.
Seems like you missed this one by not using ItemStackSnapshot.NONE.
| */ | ||
| Optional<List<ItemStack>> getResults(GridInventory grid); | ||
|
|
||
| ItemStackSnapshot getExemplaryResult(); |
There was a problem hiding this comment.
Add an empty line after the last member in a class/interface.
| * @param aisle A string array of ingredients | ||
| * @return The builder | ||
| */ | ||
| Builder aisle(@Nullable String... aisle); |
There was a problem hiding this comment.
- This method should mention what happens to the previously registered ingredient
Predicates. (Especially those, that are no loner needed for this aisle.)
Ref: where(char, Predicate) <- @throws IllegalArgumentException If the aisle does not contain the specified character symbol
There was a problem hiding this comment.
We should clarify what an 'asile' means here. If I'm correct in my understanding of it, each 'asile' corresponds to a row of the crafting grid.
| * @throws IllegalArgumentException If the aisle does not contain | ||
| * the specified character symbol | ||
| */ | ||
| Builder where(char symbol, ItemStackSnapshot ingredient) throws IllegalArgumentException; |
There was a problem hiding this comment.
I recommend adding @Nullable here to keep the remove if null part. This should also document what happens if the ItemStackSnapshot.NONE is set (IMO its the same as null => remove ingredient )
| */ | ||
| @SuppressWarnings("ConstantConditions") | ||
| default Builder result(@Nullable ItemStack result) { | ||
| return result(result != null ? result.createSnapshot() : ItemStackSnapshot.NONE); |
There was a problem hiding this comment.
This method should document the implications of this ItemStackSnapshot.NONE here.
IMO an empty stack is not a valid result.
| */ | ||
| @SuppressWarnings("ConstantConditions") | ||
| default Builder where(char symbol, @Nullable ItemStack ingredient) throws IllegalArgumentException { | ||
| return where(symbol, ingredient != null ? ingredient.createSnapshot() : ItemStackSnapshot.NONE); |
There was a problem hiding this comment.
I'm not quite sure about the NONE part. because its effectifly an invalid ingredient.
Otherwise you have to document the implications of using this. Especially since the implications are slightly different than the other methods.
| * method, as it customizes the result further depending on the specified | ||
| * {@param ingredient} {@link ItemStackSnapshot}. It is advised to use | ||
| * the output of {@link #getExemplaryResult()}, modify it accordingly, | ||
| * and {@code return} it. |
There was a problem hiding this comment.
I'm not sure whether the main javadoc part should be about the implementation. I would recommend explaining when it is used.
Out of thin air example:
This method is called at the beginning of the smelting process to check whether there is space
for the result and at the end of the smelting to calculate to actually add it to the results.
<p>Note: It is advised that the result from getExemplaryResult and this method are strictly
related to each other.
/ ... the results from this method are a derived result from getExemplaryResult </p>
| * @param result The output of this recipe | ||
| * @return This builder, for chaining | ||
| */ | ||
| Builder result(@Nullable ItemStackSnapshot result); |
There was a problem hiding this comment.
What are the implications of setting null here?
IMO setting it to null is useless since you have to set it to something none null anyway.
| * @param ingredient The required ingredient | ||
| * @return This builder, for chaining | ||
| */ | ||
| Builder ingredient(@Nullable ItemStackSnapshot ingredient); |
There was a problem hiding this comment.
Document null behaviour. See above.
| * @param ingredient The required ingredient | ||
| * @return This builder, for chaining | ||
| */ | ||
| default Builder ingredient(@Nullable ItemStack ingredient) { |
There was a problem hiding this comment.
Document null behaviour. See above.
| */ | ||
| SmeltingRecipe build(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Missing newline + one a line above
| * Builds a simple furnace recipe | ||
| */ | ||
| interface Builder extends ResettableBuilder<SmeltingRecipe, Builder> { | ||
| /** |
| * | ||
| * <p>It is advised to use the output of | ||
| * {@link CraftingRecipe#getExemplaryResult()}, modify it accordingly, | ||
| * and {@code return} it.</p> |
There was a problem hiding this comment.
There should be a hint that this comment is only for implementing classes.
Implementing classes are advised...optionally cache it and {@code return} it.
| * @return The builder | ||
| */ | ||
| default Builder result(ItemStack result) { | ||
| return result(result.createSnapshot()); |
There was a problem hiding this comment.
There should be a null check here checkNotNull(result, "result")
| * @return This builder, for chaining | ||
| */ | ||
| default Builder result(ItemStack result) { | ||
| return result(result.createSnapshot()); |
There was a problem hiding this comment.
Missing null check
return result(checkNotNull(result, "result").createSnapshot());
| * @return {@code width * height} for shaped recipes, or the number of | ||
| * ingredients for shapeless recipes | ||
| */ | ||
| int getSize(); |
There was a problem hiding this comment.
Invalid Description, It wouldn't do that if you added it after. Honestly this function isn't really necessary at all.
There was a problem hiding this comment.
I believe the order defined in the implementation is so that the crafting manager can prefer large recipes (3x3) before considering smaller 2x2 ones.
This is somewhat implementation detail that need not be exposed.
For shaped recipes the implementation just needs to do getWidth() * getHeight() and for shapeless do getIngredientPredicates().size()
| * @param world The world this recipe would be used in | ||
| * @return True if the given input matches this recipe's requirements | ||
| */ | ||
| boolean isValid(GridInventory grid, World world); |
There was a problem hiding this comment.
I'd say you should just combine this with getResult and default implement it to return false if it returns ItemTypes.AIR or null...
There was a problem hiding this comment.
In order to support listing modded recipes, I had to make CraftingRecipe symmetric to IRecipe. Making this return the ItemStackSnapshot would also unnecessarily create instances in cases they aren't needed.
| * @param ingredient The ingredient to check against | ||
| * @return Whether this {@param ingredient} can be used to craft the result | ||
| */ | ||
| boolean isValid(ItemStackSnapshot ingredient); |
There was a problem hiding this comment.
Duplicate Function? and see above...
There was a problem hiding this comment.
Not a duplicate. Extends Recipe, not CraftingRecipe.
|
|
||
| import org.spongepowered.api.item.inventory.ItemStackSnapshot; | ||
|
|
||
| public interface SmeltingResult { |
There was a problem hiding this comment.
Why is this a separate interface?
There was a problem hiding this comment.
In this case, we can afford to do the optimization you suggested for CraftingRecipe#isValid(GridInventory, World), because there is no special interface for smelting recipes in Minecraft code. This way, users don't get access to the resulting ItemStackSnapshot and the amount of experience released, unless the SmeltingRecipe#isValid(ItemStackSnapshot) returns true.
| * Checks if the given {@link GridInventory} fits the required constraints | ||
| * to craft this {@link CraftingRecipe}. | ||
| * | ||
| * @param grid The ItemGrid to check for validity |
There was a problem hiding this comment.
ItemGrid was an old name, should now be GridInventory
| * @param grid The crafting input, typically 3x3 or 2x2 | ||
| * @return An {@link ItemStackSnapshot} | ||
| */ | ||
| ItemStackSnapshot getResult(GridInventory grid); |
There was a problem hiding this comment.
Recipes can output multiple items
See this comment: #242 (comment)
Also, it's not defined what would happen if isValid returns false, does it throw an exception? Maybe it should return an Optional?
There was a problem hiding this comment.
Crafting recipes cannot. That comment talks about pulverizer recipes.
There was a problem hiding this comment.
What happens is undefined behavior, no way around that.
|
|
||
| /** | ||
| * This method should only be called if | ||
| * {@link #isValid(GridInventory, World)} returns {@code true}. |
There was a problem hiding this comment.
Again, need to define what happens if isValid returns false
There was a problem hiding this comment.
its not undefined it should throw an exception
There was a problem hiding this comment.
Well, yes, I could add a line that the method might throw a specific exception, but it does not necessarily check the validity of the input crafting grid.
| * @return The result of fulfilling the requirements of a | ||
| * {@link SmeltingRecipe} | ||
| */ | ||
| ItemStackSnapshot getResult(); |
There was a problem hiding this comment.
I think this should be a list of items. Modded recipes may return more than one item (e.g. a furnace that can smelt double at once)
There was a problem hiding this comment.
Yes, but only in "modern" GUIs. Those recipes are not supported for standard vanilla GUIs as there is only one output slot.
| private final double experience; | ||
|
|
||
| @SuppressWarnings("ConstantConditions") | ||
| public SmeltingResult(@Nonnull ItemStackSnapshot result, double experience) { |
There was a problem hiding this comment.
@Nonnull is not needed as things are nonnull by default
d29c46e to
ea6161b
Compare
| * @return The exemplary result of this recipe | ||
| */ | ||
| Optional<List<ItemStack>> getResults(GridInventory grid); | ||
| ItemStackSnapshot getExemplaryResult(); |
There was a problem hiding this comment.
I'm not sure if 'exemplary' makes sense here. What does this represent?
There was a problem hiding this comment.
I think its the item you see in the CraftingOutput before you take it out.
There was a problem hiding this comment.
The native IRecipe class contains an ItemStack getRecipeOutput(), where this value is used as the return value, and the FurnaceRecipes class contains a Map<ItemStack, ItemStack> smeltingList, where this value is used as a value in the map. I think these might be used in TooManyItems/NotEnoughItems to display the recipes, that's why I chose the word 'exemplary'.
| * @throws IllegalArgumentException If the aisle does not contain | ||
| * the specified character symbol | ||
| */ | ||
| Builder where(char symbol, @Nullable ItemStackSnapshot ingredient) throws IllegalArgumentException; |
There was a problem hiding this comment.
@Limeth: That link seems to be broken. Can the implementation take care of passing in ItemStackSnapshot.NONE to the predicate, if that's what the issue is?
| } | ||
|
|
||
| /** | ||
| * Creates a predicate with vanilla matching behavior. |
There was a problem hiding this comment.
Can you clarify what this means? Otherwise, plugins may not find it very useful.
| * @param aisle A string array of ingredients | ||
| * @return The builder | ||
| */ | ||
| Builder aisle(@Nullable String... aisle); |
There was a problem hiding this comment.
We should clarify what an 'asile' means here. If I'm correct in my understanding of it, each 'asile' corresponds to a row of the crafting grid.
|
@Aaron1011: You said, that That link seems to be broken. Can the implementation take care of passing in ItemStackSnapshot.NONE to the predicate, if that's what the issue is? |
|
@Limeth: What I'm wondering is this: Why do we need the overload of |
|
@Aaron1011 No, this is just a convenience method which uses the |
|
|
||
| @Override | ||
| public int hashCode() { | ||
| int result1 = this.result.hashCode(); |
There was a problem hiding this comment.
You can use Objects.hash(this.result, this.remainingItems) here.
|
It seems like recipes and similar will change heavily in 1.12 with some changes Dinnerbone is working on, so it may be best to revisit this with that update. |
I'd rather have recipes partially implemented for API 6 and 7 while we still can, if not fully implemented, so that when MC 1.12 comes around, we can easily update the API as necessary, along with the implementation. |
|
MC 1.12 is out and Sponge still has no working Recipe API. |
|
@Faithcaio @gabizou Not really interested in adding work back for older versions. Now that Forge 1.12 is in the wild, mods will skip 1.11 and head straight for that and leave 1.10 behind. This API needs to be updated for 1.12's changes and proceed from there. |
|
It seems he has a few changes to make here still but especially in the implementation with there being suggestions, questions, merge conflicts, and I'm assuming issues with 1.12 changes. @Limeth do you have the time and/or interest to continue these pull requests? |
|
I got discouraged by the PR process and am no longer interested in contributing. If anyone wants to take over this PR, feel free to do so. |
|
Yeah Sponge devs often respond slowly. |
|
@Limeth May I ask what discouraged you, and why you're no longer interested in contributing? |
|
@kashike It was difficult for me to adapt to the required code style, most of the responses on my PRs were only addressing the "distracting" parts of code that didn't follow the code style. This lead to much longer periods of the iterative development process (code/compile/test/debug), as I felt like I didn't get enough feedback on the actual logic I was implementing. |
|
As it has been brought up, I thought I would share https://dev.to/terrehbyte/my-pull-request-etiquette - I thought it was a rather good read, and although not all of it would be easy for Sponge to implement, I'm sure it'd be worth some though ;) There are a number of other good reads over on dev.to regarding code reviews, such as: https://dev.to/vaidehijoshi/crafting-better-code-reviews and https://dev.to/cyberomin/a-pull-request-is-submitted-what-next |
|
Superseded by #1582 |
SpongeAPI|SpongeCommon|SpongeForge
This is a WIP continuation of kashike's PR, with his consent.
It aims to generalize how recipes are registered and handled. Instead of checking the equality of the ingredients, it works by testing ingredient predicates. It also aims to let the user implement the recipe interfaces by themselves, making it, for example, possible to transfer NBT data from the ingredients to the result.
In this PR,
ItemStackSnapshots are used extensively. This is to prevent the user from modifying the originalItemStack. I could instead provide anItemStack#copy, but that doesn't make it visually obvious the original item won't change.